Skip to main content

Runoff - CS50x 2023

在这个程序中,你将实现一个模拟决胜选举的程序,如下所示:

./runoff Alice Bob Charlie
Number of voters: 5
Rank 1: Alice
Rank 2: Bob
Rank 3: Charlie

Rank 1: Alice
Rank 2: Charlie
Rank 3: Bob

Rank 1: Bob
Rank 2: Charlie
Rank 3: Alice

Rank 1: Bob
Rank 2: Alice
Rank 3: Charlie

Rank 1: Charlie
Rank 2: Alice
Rank 3: Bob

Alice

背景

您已经了解了简单多数选举,它遵循一个非常简单的算法来确定选举的获胜者:每个选民都获得一票,并且获得最多票数的候选人获胜。

但是,简单多数投票确实有一些缺点。例如,如果在有三名候选人的选举中,投了以下选票,会发生什么情况?

五张选票,Alice 和 Bob 之间平局

简单多数制会判决 Alice 和 Bob 在此打成平手,因为他们各自获得两票。但这真的是公平的结果吗?

还有另一种投票制度,叫做排序选择投票制。在排序选择制度中,选民可以投票给多名候选人。他们可以按照偏好顺序给候选人排序,而不仅仅是投票给第一选择。因此,生成的选票可能如下所示。

三张选票,具有排名偏好

在这里,除了指定他们的首选候选人之外,每位选民还指出了他们的第二和第三选择。这样一来,原本平局的选举就可能产生赢家。最初 Alice 和 Bob 并列,所以 Charlie 被淘汰。但是,投给 Charlie 的选民,他们的第二选择是 Alice 而不是 Bob,所以 Alice 就能被判定为胜出。

排序选择投票还可以解决简单多数投票的另一个潜在缺点。看看下面的选票。

九张选票,具有排名偏好

谁应该赢得这次选举?在简单多数投票中,每个选民只选择他们的首选,Charlie 以四票赢得这次选举,而 Bob 只有三票,Alice 只有两票。但是,大多数选民(9 个人里有 5 个)如果选 Alice 或者 Bob,而不是 Charlie,会更满意。通过考虑排名偏好,投票系统可以选择更能反映选民偏好的获胜者。

一种这样的排序选择投票制度就是立即决胜制。在立即决胜选举中,选民可以按照意愿给尽可能多的候选人排序。如果任何候选人获得超过半数(50%以上)的第一轮选票,该候选人就会被宣布为选举获胜者。

如果没有候选人获得超过 50% 的选票,则会发生“立即决胜”。得票最少的候选人会被淘汰出局,而最初选择该候选人为第一选择的选民,他们的第二选择将会被计入。为什么要这么做? 实际上,这模拟了如果一开始最不受欢迎的候选人就不参选的情况。

这个过程不断重复:如果没有候选人获得过半选票,那么得票最少的候选人就会被淘汰,而原本投给他们的选民,将会改投他们尚未被淘汰的下一顺位选择。一旦候选人获得多数席位,该候选人将被宣布为获胜者。

让我们以上面的九张选票为例,看看决胜选举是如何进行的。 爱丽丝得了两票,鲍勃得了三票,查理得了四票。对于一个有九个人的选举来说,需要多数票(五票)才能获胜。由于没有人获得多数票,因此需要进行第二轮投票。爱丽丝获得的票数最少(只有两票),因此爱丽丝被淘汰。原本投票给爱丽丝的选民,他们的第二选择是鲍勃,因此,鲍勃获得了这两张额外的票。鲍勃现在有五票,而查理仍然有四票。鲍勃现在获得了多数票,因此鲍勃被宣布为获胜者。

这里我们需要考虑哪些特殊情况?

一种可能是,在决定淘汰谁时出现平局。我们可以通过淘汰所有并列末位的候选人来解决这个问题。但是,如果所有剩余候选人票数相同,淘汰末位就意味着淘汰所有人!因此,在这种情况下,我们必须小心,不能淘汰所有人,只需宣布所有剩余候选人平局即可。

有些第二轮投票不需要选民对所有选项排序——例如,选举中有五位候选人,但选民可能只选两位。但是,出于本问题的目的,我们将忽略这个特殊的极端情况,并假设所有选民都将按照他们喜欢的顺序对所有候选人进行排名。

听起来是不是比简单多数投票复杂一些?但这种选举制度的优点在于,获胜者更能代表选民的意愿。

开始

登录 cs50.dev,单击您的终端窗口,然后单独执行 cd。您应该看到类似以下的终端提示符:

接下来执行

wget https://cdn.cs50.net/2022/fall/psets/3/runoff.zip

以便将名为 runoff.zip 的 ZIP 文件下载到您的 codespace 中。

然后执行

创建一个名为 runoff 的文件夹。您不再需要 ZIP 文件,因此您可以执行

并在提示符下回复“y”,然后按 Enter 键以删除您下载的 ZIP 文件。

现在输入

然后按 Enter 键将自己移动到(即打开)该目录。您的提示符现在应类似于以下内容。

如果一切顺利,您应该执行

并看到一个名为 runoff.c 的文件。执行 code runoff.c 应该会打开该文件,您将在其中键入此问题集的代码。如果不是,请回溯您的步骤,看看您是否可以确定您在哪里出错!

理解

让我们看一下 runoff.c。我们定义了两个常量:MAX_CANDIDATES 用于选举中候选人的最大数量,MAX_VOTERS 用于选举中选民的最大数量。

接下来是一个二维数组 preferencespreferences[i] 数组代表选民 i 的所有偏好,preferences[i][j] 则存储选民 i 的第 j 偏好候选人的索引。

接下来是一个名为 candidatestruct。每个 candidate 都有一个 string 字段用于他们的 name,一个 int 表示他们当前拥有的 votes 数量,以及一个名为 eliminatedbool 值,指示该候选人是否已从选举中淘汰。数组 candidates 将跟踪选举中的所有候选人。

该程序还有两个全局变量:voter_countcandidate_count

接下来我们看看 main 函数。请注意,在确定候选人和选民人数之后,主要的投票循环就开始了,让每位选民都能参与投票。选民输入偏好后,程序会调用 vote 函数来记录这些偏好。如果任何选票被判定为无效,程序就会退出。

所有选票投完后,程序会进入另一个循环。这个循环会不断重复决胜选举的流程:检查是否有获胜者,并淘汰得票最少的候选人,直到决出胜者为止。

首先,程序会调用 tabulate 函数,该函数会根据所有选民的偏好,统计每位选民尚未被淘汰的首选候选人的得票数。接着,print_winner 函数会打印出获胜者(如果存在),程序随即结束。否则,程序会调用 find_min 函数来确定仍在选举中的候选人所得的最低票数。如果所有候选人都得票相同(由 is_tie 函数判断),则宣布平局。否则,程序会调用 eliminate 函数来淘汰得票最少的候选人。

再往下看看,你会发现这些函数——votetabulateprint_winnerfind_minis_tieeliminate——都需要你来实现!

规范

完成 runoff.c 的代码编写,使其能够模拟决胜选举。你应该完成 votetabulateprint_winnerfind_minis_tieeliminate 函数的实现,并且你不应该修改 runoff.c 中的任何其他内容(以及你想要包含的额外头文件)。

vote

完成 vote 函数。

  • 这个函数接受 voterrankname 这三个参数。如果 name 与某个有效候选人的姓名一致,则更新全局偏好数组,记录选民 voter 对该候选人的偏好等级为 rank0 代表第一偏好,1 代表第二偏好,以此类推)。
  • 如果偏好已成功记录,则该函数应返回 true;否则,该函数应返回 false(例如,如果 name 不是其中一位候选人的姓名)。
  • 你可以假设没有两个候选人会同名。

提示

  • 回想一下,candidate_count 存储了选举中候选人的数量。
  • 回想一下,你可以使用 strcmp 来比较两个字符串。
  • 回想一下,preferences[i][j] 存储了候选人的索引,该候选人是第 i 位选民的第 j 个排名偏好。

tabulate

完成 tabulate 函数。

  • 这个函数需要更新每个候选人在当前阶段的得票数 (votes)。
  • 记住,在决胜选举的每个阶段,每位选民实际上都是在给他们尚未被淘汰的首选候选人投票。

提示

  • 记住,voter_count 存储了选民人数,而且本次选举中,每位选民都有一张选票。
  • 记住,对于选民 i,他们的第一选择是 preferences[i][0],第二选择是 preferences[i][1],以此类推。
  • 记住,candidate 结构体中有一个名为 eliminated 的字段,如果候选人被淘汰,该字段的值为 true
  • 记住,candidate 结构体中有一个名为 votes 的字段,你需要更新这个字段来记录每位候选人的得票数。
  • 一旦你为选民选择了第一位未被淘汰的候选人,就应该停止,不要继续遍历他们的选择!记住,你可以使用 break 语句在满足条件时跳出循环。

完成 print_winner 函数。

  • 如果任何候选人获得超过一半的选票,则应打印他们的名字,并且该函数应返回 true
  • 如果还没有人赢得选举,该函数应返回 false

提示

  • 记住,voter_count 存储了选举中选民的数量。 那么,要赢得选举需要多少票呢?

find_min

完成 find_min 函数。

  • 该函数应返回仍在选举中的任何候选人的最低票数总数。

提示

  • 你可能需要遍历所有候选人,找到仍在选举中且票数最少的那个。在遍历时,你需要记录哪些信息呢?

is_tie

完成 is_tie 函数。

  • 该函数接受一个参数 min,它代表当前候选人中的最低票数。
  • 如果选举中剩余的每个候选人都具有相同数量的选票,则该函数应返回 true,否则应返回 false

提示

  • 记住,如果仍在选举中的每个候选人都具有相同数量的选票,则会发生平局。 另请注意,is_tie 函数接收一个参数 min,它代表当前候选人中的最低票数。 你该如何利用这个信息来判断是否平局呢?

eliminate

完成 eliminate 函数。

  • 该函数接受一个参数 min,它代表当前选举中任何人拥有的最低票数。
  • 该函数应淘汰具有 min 票数的候选人(或候选人)。

讲解

用法

你的程序应按照以下示例运行:

./runoff Alice Bob Charlie
Number of voters: 5
Rank 1: Alice
Rank 2: Charlie
Rank 3: Bob

Rank 1: Alice
Rank 2: Charlie
Rank 3: Bob

Rank 1: Bob
Rank 2: Charlie
Rank 3: Alice

Rank 1: Bob
Rank 2: Charlie
Rank 3: Alice

Rank 1: Charlie
Rank 2: Alice
Rank 3: Bob

Alice

测试

请务必测试你的代码,确保它能处理…

  • 可以有任意数量候选人的选举 (最多 MAX,即 9 位)
  • 根据姓名给候选人投票
  • 对不在选票上的候选人进行无效投票
  • 如果只有一位胜出者,则打印胜出者的姓名
  • 如果所有剩余的候选人都打成平局,则不淘汰任何人

请执行以下命令,使用 check50 评估代码的正确性。但请务必自行编译并进行测试!

check50 cs50/problems/2023/x/runoff

执行以下命令,使用 style50 评估代码的风格。

如何提交

在您的终端中,执行以下命令以提交您的作品。

submit50 cs50/problems/2023/x/runoff